package cnisetup

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"gopkg.in/yaml.v3"
	v1 "k8s.io/api/core/v1"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"

	f5_bigip "github.com/f5devcentral/f5-bigip-rest-go/bigip"
	"github.com/f5devcentral/f5-bigip-rest-go/utils"
)

// func init() {
// 	slog = utils.LogFromContext(context.TODO()).WithLevel(utils.LogLevel_Type_DEBUG)
// }

func LoadConfigsFromFile(configPath string) (*CNIConfigs, error) {
	var cniconfs CNIConfigs
	if err := getConfigsFromYaml(&cniconfs, configPath); err != nil {
		return nil, err
	}

	for i := range cniconfs {
		defaultPort := 443
		if cniconfs[i].Mgmt.Port == nil {
			cniconfs[i].Mgmt.Port = &defaultPort
		}
	}

	// checks
	errs := []error{}
	for _, cc := range cniconfs {
		for _, r := range cc.Routes {
			if len(strings.Split(r.Network, "/")) != 2 {
				errs = append(errs, fmt.Errorf("invalid route network '%s': should be format of a.b.c.d/e", r.Network))
			}
		}
	}
	return &cniconfs, errors.Join(errs...)
}

func LoadConfigFromDataGroup(bc *f5_bigip.BIGIPContext) (*CNIConfig, error) {
	slog := utils.LogFromContext(bc)
	bcnf, err := bc.LoadDataGroup(CNIConfigDGName, "Common")
	if err != nil {
		return nil, err
	}
	if bcnf == nil {
		slog.Infof("no config '%s' found from data-group", CNIConfigDGName)
		return nil, nil
	}

	var cniconf CNIConfig
	err = json.Unmarshal(bcnf, &cniconf)
	return &cniconf, err
}

func getConfigsFromYaml(CNIConfigs *CNIConfigs, configPath string) error {
	fn := configPath
	f, err := os.Open(fn)
	if err != nil {
		return fmt.Errorf("failed to open file %s for reading: %s", fn, err.Error())
	}
	defer f.Close()
	byaml, err := io.ReadAll(f)
	if err != nil {
		return fmt.Errorf("failed to read file: %s: %s", fn, err)
	}
	if err := yaml.Unmarshal(byaml, &CNIConfigs); err != nil {
		return fmt.Errorf("failed to unmarshal yaml content: %s", err.Error())
	}
	return nil
}

func deploy(bc *f5_bigip.BIGIPContext, partition string, ocfgs, ncfgs *map[string]interface{}) error {
	kinds := f5_bigip.GatherKinds(ocfgs, ncfgs)
	existings, err := bc.GetExistingResources(partition, kinds)
	if err != nil {
		return err
	}
	cmds, err := bc.GenRestRequests(partition, ocfgs, ncfgs, existings)
	if err != nil {
		return err
	}
	return bc.DoRestRequests(cmds)
}

func enableBGPRouting(bc *f5_bigip.BIGIPContext) error {
	kind := "net/route-domain"
	partition, subfolder, name := "Common", "", "0" // route domain 0

	exists, err := bc.Exist(kind, name, partition, subfolder)
	if err != nil {
		return err
	}
	if exists == nil {
		return fmt.Errorf("route domain 0 must exist. check it")
	}
	// "Cannot mix routing-protocol Legacy and TMOS mode for route-domain (/Common/0)."
	// We need to remove "BGP" from routingProtocol for TMOS mode
	if (*exists)["routingProtocol"] != nil {
		nrps := []interface{}{}
		for _, rp := range (*exists)["routingProtocol"].([]interface{}) {
			if rp.(string) != "BGP" {
				nrps = append(nrps, rp)
			}
		}
		body := map[string]interface{}{
			"routingProtocol": nrps,
		}
		if err := bc.Update(kind, name, partition, subfolder, body); err != nil {
			return err
		}
	}

	return bc.ModifyDbValue("tmrouted.tmos.routing", "enable")
}

func newRestConfig(kubeConfig string) *rest.Config {
	var config *rest.Config
	var err error

	if kubeConfig == "" {
		config, err = rest.InClusterConfig()
		if nil != err {
			panic(err)
		}
	} else if _, err := os.Stat(kubeConfig); err == nil {
		config, err = clientcmd.BuildConfigFromFlags("", kubeConfig)
		if nil != err {
			panic(err)
		}
	} else {
		config, err = clientcmd.RESTConfigFromKubeConfig([]byte(kubeConfig))
		if nil != err {
			panic(err)
		}
	}
	return config
}

func newKubeClient(kubeConfig string) *kubernetes.Clientset {
	config := newRestConfig(kubeConfig)
	client, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err.Error())
	}
	return client
}

func newDynamicClient(kubeConfig string) *dynamic.DynamicClient {
	config := newRestConfig(kubeConfig)
	client, err := dynamic.NewForConfig(config)
	if err != nil {
		panic(err.Error())
	}
	return client
}

func macAddrOfTunnel(bc *f5_bigip.BIGIPContext, name string) (string, error) {
	slog := utils.LogFromContext(bc.Context)
	// modify sys db tmrouted.tmos.routing value enable
	cmd := fmt.Sprintf(`show net tunnels tunnel %s all-properties`, name)
	slog.Debugf("tmsh cmd: %s", cmd)

	reMac := regexp.MustCompile("(?:[0-9a-fA-F]:?){12}")

	for times, waits := 30, time.Millisecond*100; times > 0; times-- {
		if resp, err := bc.Tmsh(cmd); err != nil {
			return "", err
		} else {
			if rlt, ok := (*resp)["commandResult"]; ok {
				macAddress := reMac.FindString(rlt.(string))
				if macAddress == "" {
					slog.Warnf("not found macAddress from %s", (*resp)["commandResult"].(string))
				} else {
					slog.Debugf("got tunnel %s macAddress: %s", name, macAddress)
					return macAddress, nil
				}
			} else {
				slog.Warnf("empty response from tmsh '%s', no macAddress retrived", cmd)
			}
			<-time.After(waits)
		}
	}

	return "", fmt.Errorf("timeout for getting tunnel mac address")
}

func allNodeIpAddrs(ctx context.Context, ns *v1.NodeList) []string {
	rlt := []string{}
	ipv4, ipv6 := allNodeIPMacAddrs(ctx, ns)
	for k := range ipv4 {
		rlt = append(rlt, k)
	}
	for k := range ipv6 {
		rlt = append(rlt, k)
	}
	return rlt
}

func nodeIsTaint(n *v1.Node) bool {
	for _, taint := range n.Spec.Taints {
		if taint.Key == "node.kubernetes.io/unreachable" && taint.Effect == "NoSchedule" {
			return true
		}
	}
	return false
}

func allNodeIPMacAddrs(ctx context.Context, ns *v1.NodeList) (map[string]string, map[string]string) {
	slog := utils.LogFromContext(ctx)
	rlt4 := map[string]string{}
	rlt6 := map[string]string{}

	for _, n := range ns.Items {
		if nodeIsTaint(&n) {
			continue
		}
		ipaddrv4, ipaddrv6 := "", ""
		macv4, macv6 := "", ""
		// calico
		if _, ok := n.Annotations["projectcalico.org/IPv4Address"]; ok {
			// no mac addr found in 'kubectl get nodes -o yaml'
			ipmask := n.Annotations["projectcalico.org/IPv4Address"]
			ipaddrv4 = strings.Split(ipmask, "/")[0]
		} else {
			// TODO: support Cilium and Flannel host-gw mode
			// flannel v4
			if _, ok := n.Annotations["flannel.alpha.coreos.com/backend-data"]; ok {
				ipmask := n.Annotations["flannel.alpha.coreos.com/public-ip"]
				ipaddrv4 = strings.Split(ipmask, "/")[0]
				macStr := n.Annotations["flannel.alpha.coreos.com/backend-data"]
				var v map[string]interface{}
				err := json.Unmarshal([]byte(macStr), &v)
				if err != nil {
					slog.Errorf("failed to get mac of %s: %s", n.Name, err.Error())
				}
				macv4 = v["VtepMAC"].(string)
			}
			// flannel v6
			if _, ok := n.Annotations["flannel.alpha.coreos.com/backend-v6-data"]; ok {
				ipaddrv6 = n.Annotations["flannel.alpha.coreos.com/public-ipv6"]
				macStrV6 := n.Annotations["flannel.alpha.coreos.com/backend-v6-data"]
				var v6 map[string]interface{}
				err := json.Unmarshal([]byte(macStrV6), &v6)
				if err != nil {
					slog.Errorf("failed to get mac v6 of %s: %s", n.Name, err.Error())
				}
				macv6 = v6["VtepMAC"].(string)
			}
		}
		if ipaddrv4 != "" {
			rlt4[ipaddrv4] = macv4
		}
		if ipaddrv6 != "" {
			rlt6[ipaddrv6] = macv6
		}

	}
	return rlt4, rlt6
}

func allNodesIP2Macs(ctx context.Context, ns *v1.NodeList) (map[string]string, map[string]string) {
	rlt4 := map[string]string{}
	rlt6 := map[string]string{}

	for _, n := range ns.Items {
		addrs := n.Status.Addresses
		ipaddr := ""
		for _, addr := range addrs {
			if addr.Type == v1.NodeInternalIP {
				ipaddr = addr.Address
				break
			}
		}
		rlt4[ipaddr] = ipv4ToMac(ipaddr)
	}

	return rlt4, rlt6
}

func allNodeIPCIDRs(ctx context.Context, ns *v1.NodeList) map[string]string {
	rlt := map[string]string{}

	for _, n := range ns.Items {
		addrs := n.Status.Addresses
		ipaddr := ""
		for _, addr := range addrs {
			if addr.Type == v1.NodeInternalIP {
				ipaddr = addr.Address
				break
			}
		}
		for _, p := range n.Spec.PodCIDRs {
			rlt[p] = ipaddr
		}
	}

	return rlt
}

func parseNeighsFrom(name, remoteAS string, ips []string) map[string]interface{} {
	rlt := map[string]interface{}{}
	for _, address := range ips {
		resName := fmt.Sprintf("net/routing/bgp/~%s~%s/neighbor/%s", "Common", name, address)
		rlt[resName] = map[string]interface{}{
			"name":     address,
			"remoteAs": remoteAS,
		}
	}

	return rlt
}

func parseFdbsFrom(tunnelName string, iPToMac map[string]string) map[string]interface{} {
	rlt := map[string]interface{}{}

	for ip, mac := range iPToMac {
		resName := fmt.Sprintf("net/fdb/tunnel/~%s~%s/records/%s", "Common", tunnelName, mac)
		rlt[resName] = map[string]interface{}{
			"name":     mac,
			"endpoint": ip,
		}
	}
	return rlt
}

func parseRoutesFrom(cidr2ips map[string]string) map[string]interface{} {
	rlt := map[string]interface{}{}
	for cidr, ip := range cidr2ips {
		ns := strings.Split(cidr, "/")
		rlt["net/route/"+ns[0]] = map[string]interface{}{
			"name":    ns[0],
			"network": cidr,
			"gw":      ip,
		}
	}
	return rlt
}

// TODO: auto detect cnitype.

// TODO: fix the f5-bigip-rest issue:
//
//	The tunnel (/Common/fl-vxlan) cannot be modified or deleted because it is in use by a VXLAN tunnel (/Common/fl-tunnel).
func parseVxlanProfile(name string, port int, floodingType string) map[string]interface{} {
	return map[string]interface{}{
		"name":         name,
		"floodingType": floodingType,
		"port":         float64(port), // same type as retrieved from bigip
	}
}

func parseTunnel(name, key, address, profile string) map[string]interface{} {
	return map[string]interface{}{
		"name":         name,
		"key":          key,
		"localAddress": address,
		"profile":      profile,
	}
}

func parseSelf(name, address, vlan string) map[string]interface{} {
	return map[string]interface{}{
		"name":         name,
		"address":      address,
		"vlan":         vlan,
		"allowService": "all",
	}
}

func parseVlan(name string, tag int, infs []Interface) map[string]interface{} {
	ifs := []map[string]interface{}{}
	for _, inf := range infs {
		ifs = append(ifs, map[string]interface{}{
			"name":   inf.Name,
			"tagged": inf.Tagged,
		})
	}
	return map[string]interface{}{
		"name":       name,
		"tag":        tag,
		"interfaces": ifs,
	}

}

func parseBGP(routerName, localAs, remoteAs string) map[string]interface{} {

	// TODO:
	// failed to create target config to bigip [10.250.11.187]: 400,
	// 	{"code":400,"message":"transaction failed:01071c4b:3: Invalid BGP Instance (/Common/k8s-bgp-cluster22):
	// 	can not have more than one BGP instance that have view attribute disabled per route-domain.","errorStack":[],
	//	"apiError":2}
	return map[string]interface{}{
		"name":    routerName,
		"localAs": localAs,
		// "neighbor": []interface{}{},
		// to fix this:
		// 	{"code":400,"message":"transaction failed:
		//	one or more properties must be specified","errorStack":[],"apiError":2}
		"enabled": "true",
	}
}

func parseRoute(net, intf string) map[string]interface{} {
	rn := strings.Split(net, "/")
	return map[string]interface{}{
		"tmInterface": intf,
		"name":        rn[0],
		"network":     net,
	}
}

// Convert an IPV4 string to a fake MAC address.
func ipv4ToMac(addr string) string {
	ip := strings.Split(addr, ".")
	if len(ip) != 4 {
		return ""
	}
	var intIP [4]int
	for i, val := range ip {
		intIP[i], _ = strconv.Atoi(val)
	}
	return fmt.Sprintf("0a:0a:%02x:%02x:%02x:%02x", intIP[0], intIP[1], intIP[2], intIP[3])
}
